fix(llm): retry transient network errors once before failing polish#443
Conversation
用户反馈日志显示 streaming polish 偶发失败:
[coord] streaming polish FAILED: network error:
error sending request for url (https://api.deepseek.com/v1/chat/completions)
诊断:失败发生在 reqwest `request.send().await` 阶段(不是 HTTP 4xx/5xx),
属于 connect / request / timeout 三类 transient 错误。同一 session 内大量
polish 调用成功,证明网络没断、是间歇性抖动。
修法:抽 `send_with_transient_retry` helper,首次失败 + 错误是 transient
(is_connect / is_request / is_timeout)→ sleep 500ms 重试一次。retry 第二次
失败按原 LLMError::Timeout / Network 返回。HTTP 4xx/5xx 走 response.status()
分支不受影响。
适用范围:3 处 reqwest send 都用 helper:
- chat_completion_messages_streaming (SSE 流式,line 720)
- chat_completion_messages_streaming 的非流式兄弟 (line 591)
- send_chat_request 一次性 chat 调用 (line 517)
retry 安全前提:传入 RequestBuilder body 必须是内存型(json/form),用
`try_clone()` 复制;3 处都满足。对流式路径 retry 安全是因为失败发生在 SSE
开始前 → on_delta 必然未被调用 → 不会重复输出。
注:Codex OAuth 路径(line 1085)使用独立 client,结构不同,本 PR 不动;
后续如有相同抖动反馈再扩展。
cargo test 263 全过。
PR Reviewer Guide 🔍(Review updated until commit 55c0c64)Here are some key observations to aid the review process:
|
…443 round 2) pr_agent #443 review 指出 Duplicate Request 风险: > Retrying after send() fails can submit the same LLM request twice if the > first attempt already reached the provider but the connection dropped > before a response was returned. For non-idempotent completion calls, that > can mean duplicate billing and two completions for one user action. 事实分析 reqwest::Error 各 variant: - is_connect() → TCP 握手没建立,server 不可能收到 → 安全 retry - is_request() → HTTP 请求层错误(构造问题),server 没收到完整请求 → 安全 retry - is_timeout() → client 设置的 timeout 到了,server 可能已经收到并在处理 (非幂等 completion 调用)→ 不安全 retry,会重复 billing 修法:从 retry 触发条件移除 `|| e.is_timeout()`。timeout 直接返回 LLMError::Timeout,不再重试。 user 实际看到的失败模式是 "error sending request"(reqwest::is_connect),不在 被移除范围内,覆盖率不变。 注:pr_agent 同轮提的 "Ticket compliance ❌ Not compliant 442" 是 false positive —— pr_agent 把 PR description 里提到的 #442 当成本 PR 的 ticket id;#442 是另一 个独立 PR(A-D 默认值 / 提示词重构),已 merge。 cargo test 263 全过。
|
round-2 commit 55c0c64 解决了 pr_agent 的 Duplicate Request 关切:移除 retry 条件中的 另:pr_agent 同轮的 Ticket compliance ❌ Not compliant 442 是 false positive。pr_agent 把 PR description 提到的 #442 当成本 PR 的 ticket id;#442 是另一个独立 PR(A-D 默认值 / 提示词重构),已 merge。本 PR 范围仅 LLM 网络层 retry。 |
|
Persistent review updated to latest commit 55c0c64 |
|
回应 pr_agent round 2 两个反馈: Ticket compliance ❌ Not compliant 442 — 重复 false positive。pr_agent 把 PR description 提到的 #442 当成本 PR 的 ticket id;#442 是另一个独立 PR(A-D),已 merge。本 PR 范围 = LLM 网络层 retry。 Retry safety — 工程 trade-off:
权衡选保留 合并继续。 |
User description
Summary
用户反馈 streaming polish 偶发失败,日志显示:
```
[ERROR] [coord] streaming polish FAILED: network error:
error sending request for url (https://api.deepseek.com/v1/chat/completions)
```
出现 4 次(02:53 / 03:17 / 03:23 / 03:25),夹在大量成功 polish 之间——证明网络没断、是间歇性抖动(DNS / TLS handshake / connection reset / 链路超时)。
改动
抽 `send_with_transient_retry` helper(polish.rs 末尾),首次失败 + 错误是 transient(`is_connect` / `is_request` / `is_timeout`)→ sleep 500ms 重试一次。HTTP 4xx/5xx 走 `response.status()` 分支不受影响。
3 处 reqwest send 都用 helper:
安全前提
不变
Test plan
跟 #442 关系
独立 PR。#442 是「默认值 + 提示词重构」;本 PR 是「LLM 网络层 retry」,两者无冲突。merge 顺序无所谓。
PR Type
Bug fix
Description
Retry transient request send failures once
Skip retries for timeout errors
Reuse helper across three LLM paths
Preserve existing HTTP status handling
Diagram Walkthrough
File Walkthrough
polish.rs
Add transient retry for LLM requestsopenless-all/app/src-tauri/src/polish.rs
request.send().awaitcalls withsend_with_transient_retry.is_connect()andis_request()failures.
is_timeout()as a hard failure to avoid duplicate billing.streaming safety.